/**
 * ParticleCanvas.java
 * Part of the Particle Physics Simulator
 * By Wilson Burhan and Tyler Simrell
 * Code based on code written by Matthew Alden
 * Version 1.0
 * Date Finished: 5/8/13
 */

import com.sun.j3d.utils.universe.*;
import java.awt.event.*;

import javax.media.j3d.*;
import javax.swing.*;
import javax.vecmath.*;

/**
 * This class is the major workspace of the particle simulator. It contains
 * all the graphics along with the physics behind the graphics. Based on code
 * written by Matthew Alden.
 * @author Tyler Simrell and Wilson Burhan
 *
 */
@SuppressWarnings("serial")
public class ParticleCanvas extends Canvas3D implements ActionListener
{
	/**
	 *  Physics updates per second (approximate).
	 */
	private static final int UPDATE_RATE = 60;
	/**
	 *  Width of the extent in meters.
	 */
	private static final float EXTENT_WIDTH = 20;
	
	/**
	 * The amount of rotation done by the keyboard
	 */
	private static final double ANGLE_DELTA = .1;
	/**
	 * The amount of space that gravity moves particles
	 */
	private static final double GRAVITY_CONSTANT = -20;
	/**
	 * The amount of force that the wind applies to the particles
	 */
	private static final double WIND_CONSTANT = -20;
	/**
	 * The speed of the singularity pulling in particles
	 */
	private static final float SINGULARITY_SPEED = 2;
	/**
	 * The amount of particles on screen at once.
	 */
	private static final int PARTICLE_AMOUNT = 100;
	
	private CubeFace[] boundaries;
	private Particle[] particles;
	private Transform3D rotation;
	private TransformGroup world;
	private BranchGroup trueScene;
	private double gravity = GRAVITY_CONSTANT;
	private double wind = 0;
	private float cor = 1f;
	private BranchGroup scene;
	private boolean singularity = false;
	public boolean isVectorField = false;
	private BranchGroup vectorFieldBG;
	public boolean toggleCollision = false;
	

	public ParticleCanvas() 
	{		 		
		super(SimpleUniverse.getPreferredConfiguration());
		setUp();
	}
	
	/**
	 * This method sets up much of the major graphical groups along with
	 * creating the particles and extent. It also sets off the timer object.
	 */
	private void setUp() 
	{
		// Fix for background flickering on some platforms
		System.setProperty("sun.awt.noerasebackground", "true");

		SimpleUniverse simpleU = new SimpleUniverse(this);
		simpleU.getViewingPlatform().setNominalViewingTransform();
		simpleU.getViewer().getView().setSceneAntialiasingEnable(true);

		// Add a scaling transform that resizes the virtual world to fit
		// within the standard view frustum.
		trueScene = new BranchGroup();
		vectorFieldBG = new BranchGroup();
		vectorFieldBG.setCapability(BranchGroup.ALLOW_DETACH);
		TransformGroup worldScaleTG = new TransformGroup();
		final Transform3D t3D = new Transform3D();
		t3D.setScale(.65 / EXTENT_WIDTH);
		worldScaleTG.setTransform(t3D);
		trueScene.addChild(worldScaleTG);
		rotation = new Transform3D();
		world = new TransformGroup();
		world.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
		world.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
		world.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		world.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		world.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		worldScaleTG.addChild(world);
		scene = new BranchGroup();
		world.addChild(scene);
		
		final TransformGroup extentTransform = new TransformGroup();
		extentTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		extentTransform.addChild(createExtent());
		scene.addChild(extentTransform);
		createParticles();
		for (Particle p : particles)
			scene.addChild(p.BG);
		
		simpleU.addBranchGraph(trueScene);
		createKeyListener();
		new Timer(1000 / UPDATE_RATE, this).start();	
	}
	
	/**
	 * This method creates all of the particles along with setting up the
	 * extent. 
	 */
	private void createParticles()
	{
		boundaries = new CubeFace[] {new CubeFace(0, 0, EXTENT_WIDTH/2, 0, 0, -1),
                					 new CubeFace(0, 0, -EXTENT_WIDTH/2, 0, 0, 1),
					                 new CubeFace(EXTENT_WIDTH/2, 0, 0, -1, 0, 0),
					                 new CubeFace(-EXTENT_WIDTH/2,0,0,1, 0, 0),
									 new CubeFace(0, EXTENT_WIDTH/2, 0,0,-1,0),
					                 new CubeFace(0,-EXTENT_WIDTH/2,0,0,1,0)};
		particles = new Particle[PARTICLE_AMOUNT];
		for (int i = 0; i < particles.length; i++)
		{
			particles[i] = new Particle(1, (float)(Math.random() - .5) * EXTENT_WIDTH, (float)(Math.random() - .5) * EXTENT_WIDTH, (float)(Math.random() - .5) * EXTENT_WIDTH, 0, 0, 0, EXTENT_WIDTH * .008f, null);
		
		}
	}
	
	/**
	 * This method creates the key listeners to allow the keyboard to
	 * rotate the extent cube.
	 */
	private void createKeyListener()
	{
		this.addKeyListener(new KeyListener() {
			@Override
			public void keyTyped(KeyEvent e) {
			}
			@Override
			public void keyReleased(KeyEvent e) {
			}
			@Override
			public void keyPressed(KeyEvent e) 
			{
				Transform3D newXRotate = new Transform3D();
				Transform3D newYRotate = new Transform3D();
				int key = e.getKeyCode();
				switch(key)
				{
					case KeyEvent.VK_UP:
					{
						newXRotate.rotX(ANGLE_DELTA);
						break;
					}
					
					case KeyEvent.VK_DOWN:
					{
						newXRotate.rotX(-ANGLE_DELTA);
						break;
					}
					
					case KeyEvent.VK_LEFT:
					{
						newYRotate.rotY(-ANGLE_DELTA);
						break;
					}
					
					case KeyEvent.VK_RIGHT:
					{
						newYRotate.rotY(ANGLE_DELTA);
						break;
					}
				}
				
				newXRotate.mul(newYRotate);
				newXRotate.mul(rotation);
				rotation = newXRotate;
				world.setTransform(rotation);
			}
		});
	}
	
	/**
	 * This method goes through every particle and updates it's 
	 * position and velocity to allow for the particles to move
	 * somewhat realistically.
	 */
	private void tick() 
	{
		for (Particle p : particles) 
		{
			p.forceAccumulator.y = (float) (gravity * p.mass);
			p.forceAccumulator.x = (float) wind/2;
			p.forceAccumulator.z = (float) wind;
			 
			if(singularity)
			{
				if(p.position.x < 0)
				{
					p.velocity.x = SINGULARITY_SPEED*Math.abs(p.position.x);
				}
				
				if(p.position.x > 0)
				{
					p.velocity.x = -SINGULARITY_SPEED*Math.abs(p.position.x);
				}
				
				if(p.position.y < 0)
				{
					p.velocity.y = SINGULARITY_SPEED*Math.abs(p.position.y);
				}
				
				if(p.position.y > 0)
				{
					p.velocity.y = -SINGULARITY_SPEED*Math.abs(p.position.y);
				}
				
				if(p.position.z < 0)
				{
					p.velocity.z = SINGULARITY_SPEED*Math.abs(p.position.z);
				}
				
				if(p.position.z > 0)
				{
					p.velocity.z = -SINGULARITY_SPEED*Math.abs(p.position.z);
				}
			}		
			p.updateState(1f / UPDATE_RATE);
			for (CubeFace hs : boundaries)
				checkAndResolveCollision(hs, p);
			// Particle Collision
			if(toggleCollision) {
				for (Particle c2 : particles)
					checkAndResolveCollision(c2, p);
			}
			p.updateTransformGroup();
			
			// Clear the particle's force accumulator.
			p.forceAccumulator.x = p.forceAccumulator.y = p.forceAccumulator.z = 0;
		}
	}

	/**
	 * This method checks and resolves collisions of the particles
	 * and the extent. It then resolves any collisions.
	 * @param hs The extent
	 * @param p The particle that is being checked
	 */
	private void checkAndResolveCollision(CubeFace hs, Particle p) {
		float distance = hs.normal.dot(p.position) - hs.intercept;
		if (distance < 0) {
			// Use Torricelli's equation to approximate the particle's
			// velocity (v_i) at the time it contacts the CubeFace.
			// v_f^2 = v_i^2 + 2 * acceleration * distance
			
			// Final velocity of the particle in the direction of the CubeFace normal
			float v_f = hs.normal.dot(p.velocity);
			// Velocity of the particle in the direction of the CubeFace normal at the
			// time of contact, squared
			float v_i_squared = v_f * v_f - 2 * p.forceAccumulator.dot(hs.normal) * distance;
			// If v_i_squared is less than zero, then the quantities involved are so small
			// that numerical inaccuracy has produced an impossible result.  The velocity
			// at the time of contact should therefore be zero.
			if (v_i_squared < 0)
				v_i_squared = 0;
			// Remove the incorrect velocity acquired after the contact and 
			//add the flipped
			// correct velocity.
			p.velocity.scaleAdd(-v_f + cor * (float)Math.sqrt(v_i_squared), 
					hs.normal, p.velocity);
			
			// Old code for adjusting the velocity. 
			// p.velocity.scaleAdd(-(1 + COEFFICIENT_OF_RESTITUTION) * 
			//hs.normal.dot(p.velocity), hs.normal, p.velocity);
			
			p.position.scaleAdd(-distance, hs.normal, p.position);
		}
	}
	
	/**
	 * This method checks and resolves whether or not a particle has hit
	 * another particle
	 * @param c2 A particle that may or may not be hit.
	 * @param c The other particle in question that may or may have been hit.
	 */
	private void checkAndResolveCollision(Particle c2, Particle c) {
		if (c2 == c)
			return;
		
		Vector3f normal = new Vector3f();
		normal.scaleAdd(-1, c2.position, c.position);
		float distance = normal.length() - EXTENT_WIDTH * .008f - 
				EXTENT_WIDTH * .008f;
		if (distance < 0) {
			normal.normalize();
			float v1 = c.velocity.dot(normal);
			float v2 = c2.velocity.dot(normal);
			if (v1 - v2 > 0)
				return;
			float v1Prime = ((c.mass - 1 * c2.mass) * v1 + 
					(1 + 1) * c2.mass * v2) / (c.mass + c2.mass);
			float v2Prime = ((c2.mass - 1 * c.mass) * v2 + 
					(1 + 1) * c.mass * v1) / (c.mass + c2.mass);
			c.velocity.scaleAdd(-v1 + v1Prime, normal, c.velocity);
			c2.velocity.scaleAdd(-v2 + v2Prime, normal, c2.velocity);
			
			c.position.scaleAdd(-distance * c2.mass / (c.mass + c2.mass), 
					normal, c.position);
			c2.position.scaleAdd(distance * c.mass / (c.mass + c2.mass), 
					normal, c2.position);
		}
	}
	
	/**
	 * This method creates the vector field. Inside of it, it creates
	 * the lines necessary to show what forces are being exerted on the 
	 * particles
	 * @return This method returns the vector lines as a Shape3D that can
	 *         be added a Branch Group. 
	 */
	private Shape3D vectorField() 
	{
		Appearance app = new Appearance();
		int pointArraySize = 250;
	    ColoringAttributes ca = new ColoringAttributes(new 
	    		Color3f(1.0f, 1.0f, 1.0f), ColoringAttributes.SHADE_FLAT);
	    app.setColoringAttributes(ca);
		Point3f[] pointArray = new Point3f[pointArraySize];
		float xCoord;
		float yCoord;
		float zCoord;
		Point3f modifier = new Point3f();
		int arrayIndex = 0;
		
		if(gravity < 0)
		{
			modifier.x += 0;
			modifier.y += (float) gravity / 10;
			modifier.z += 0;
		}
		
		if(wind < 0)
		{
			modifier.x += (float) (wind/2)/10; //It is written this way to show
											   //that they are for different reasons.
			modifier.y += 0;
			modifier.z += (float) (wind)/10;
		}
		
		if(wind >= 0 && gravity >= 0)
		{
			modifier.x += .1;
			modifier.y += 0;
			modifier.z += 0;
		}
		
		if(singularity)
		{
			pointArray = vectorFieldSingularity(pointArray);
		}
		
		else
		{
			for(int zIndex = 1; zIndex <= 5; zIndex++)
			{
				zCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * zIndex);
				for(int yIndex = 1; yIndex <= 5; yIndex++) 
				{
					yCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * yIndex);
					for(int xIndex = 1; xIndex <= 5; xIndex++) 
					{
						xCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * xIndex);
						pointArray[arrayIndex] = new Point3f(xCoord, yCoord, zCoord);
						arrayIndex++;
						pointArray[arrayIndex] = new Point3f
								(xCoord + modifier.x, 
										 yCoord + modifier.y, 
										 zCoord + modifier.z);
						arrayIndex++;	
					}
				}
			}
	    }
		LineArray lineArray = new LineArray(pointArraySize, LineArray.COORDINATES|LineArray.COLOR_3);
		Color3f[] colorArray = new Color3f[pointArraySize];
		
		for(int i = pointArraySize - 1; i >= 0; i--)
		{
			if(i % 2 == 0)
			{
				colorArray[i] = new Color3f(1.0f, 1.0f, 1.0f);
			}
			else
			{
				colorArray[i] = new Color3f(0, 1.0f, 0);
			}
		}
		
		lineArray.setCoordinates(0, pointArray);
		lineArray.setCapability(GeometryArray.ALLOW_COLOR_WRITE);
		lineArray.setCapability(GeometryArray.ALLOW_COLOR_READ);
		lineArray.setColors(0, colorArray);
		Shape3D lineShape = new Shape3D(lineArray);
		return lineShape;
	}
	
	/**
	 * This method is the version of the Vector Field method
	 * that specifically deals with singularity mode. To reduce
	 * complexity in the original Vector Field method, we decided
	 * to pull all of the singularity portions away from the other method.
	 * This one is mostly the same, except it does math to figure out the
	 * amount of force that each line should represent.
	 * @param points The points array is empty when it comes in. It is
	 *               but a shell, ready to hold the points for the Vector Field
	 *               lines.
	 * @return This method returns the Point3f array that is filled with the
	 *         coordinates of the Vector Field lines.
	 */
	private Point3f[] vectorFieldSingularity(Point3f[] points)
	{
		float zCoord;
		float yCoord;
		float xCoord;
		int arrayIndex = 0;
		
		for(int zIndex = 1; zIndex <= 5; zIndex++)
		{
			zCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * zIndex);
			for(int yIndex = 1; yIndex <= 5; yIndex++) 
			{
				yCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * yIndex);
				for(int xIndex = 1; xIndex <= 5; xIndex++) 
				{
					xCoord = -EXTENT_WIDTH/2 + ((EXTENT_WIDTH/6) * xIndex);
					points[arrayIndex] = new Point3f(xCoord, yCoord, zCoord);
					arrayIndex++;
					float tempXCoord = xCoord + (0 - xCoord)/10;
					float tempYCoord = yCoord + (0 - yCoord)/10;
					float tempZCoord = zCoord + (0 - zCoord)/10;
					points[arrayIndex] = new Point3f(tempXCoord, tempYCoord, tempZCoord);
					arrayIndex++;
					
				}
			}
		}
		return points;
	}

	/**
	 * This method creates the extent.
	 * @return The created extent in the form of a Shape3D.
	 */
	private Node createExtent() {
		float[] coordinates = {-EXTENT_WIDTH/2, EXTENT_WIDTH/2, EXTENT_WIDTH/2,
							   -EXTENT_WIDTH/2, -EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, -EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       
		                       -EXTENT_WIDTH/2, EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       -EXTENT_WIDTH/2, EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       -EXTENT_WIDTH/2, -EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       -EXTENT_WIDTH/2, -EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       
		                       -EXTENT_WIDTH/2, -EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, -EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, -EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, -EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       
		                       EXTENT_WIDTH/2, EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, EXTENT_WIDTH/2, EXTENT_WIDTH/2,
		                       EXTENT_WIDTH/2, EXTENT_WIDTH/2, -EXTENT_WIDTH/2,
		                       -EXTENT_WIDTH/2, EXTENT_WIDTH/2, -EXTENT_WIDTH/2};        
		
		LineStripArray geometry = new LineStripArray(16, GeometryArray.COORDINATES, new int[] {16});
		
		geometry.setCoordinates(0, coordinates);
		Shape3D shape = new Shape3D(geometry);
		Appearance appearance = new Appearance();
		appearance.setColoringAttributes(new ColoringAttributes(1f, 1f, 1f, ColoringAttributes.FASTEST));
		shape.setAppearance(appearance);
		
		return shape;
	}
	
	/**
	 * This method contains the math involved with the Big Bang mode.
	 */
	private void bigBang()
	{
		for(Particle p: particles)
		{
			p.velocity.x = (float) ((float) Math.signum(Math.random() - Math.random())*(Math.random() * 10));
			p.velocity.y = (float) ((float) Math.signum(Math.random() - Math.random())*(Math.random() * 10));
			p.velocity.z = (float) ((float) Math.signum(Math.random() - Math.random())*(Math.random() * 10));
		}
	}
	
	/**
	 * This method is the go between for the canvas and the frame. It toggles
	 * the gravity on or off depending on its current state.
	 * @return Whether or not the gravity is on or off.
	 */
	public boolean toggleGravity()
	{
		boolean isGravityOn;
		if(gravity == GRAVITY_CONSTANT)
		{
			gravity = 0;
			isGravityOn = false; // Gravity will be turned off
		}
		else
		{
			gravity = GRAVITY_CONSTANT;
			isGravityOn = true;
		}
		
		if(isVectorField)
		{
			world.removeChild(vectorFieldBG);
			vectorFieldBG.removeAllChildren();
			vectorFieldBG.addChild(vectorField());
			world.addChild(vectorFieldBG);
		}
		
		return isGravityOn;
	}
	
	/**
	 * This method is the go between for the canvas and the frame regarding
	 * the wind. It toggles on or off the wind depending on the current state.
	 * @return The current state of the wind.
	 */
	public boolean toggleWind() 
	{
		boolean isWindOn;
		if(wind == WIND_CONSTANT)
		{
			wind = 0;
			isWindOn = false;
		}
		else
		{
			wind = WIND_CONSTANT;
			isWindOn = true;
		}
		
		if(isVectorField)
		{
			world.removeChild(vectorFieldBG);
			vectorFieldBG.removeAllChildren();
			vectorFieldBG.addChild(vectorField());
			world.addChild(vectorFieldBG);
		}
		
		return isWindOn;
	}
	
	/**
	 * This method toggles the singularity mode. When toggled on, gravity
	 * and wind forces are turned off as they don't work with the singularity
	 * force. If the singularity is already going, running this method
	 * will launch the big bang force, which does allow the other forces to
	 * be used.
	 * @return The current state of the singularity/big bang forces.
	 */
	public boolean toggleSingularity()
	{
		if(singularity == false)
		{
			singularity = true;
			gravity = 0;
			wind = 0;
		}
		else
		{
			singularity = false;
			bigBang();
		}
		
		if(isVectorField)
		{
			world.removeChild(vectorFieldBG);
			vectorFieldBG.removeAllChildren();
			vectorFieldBG.addChild(vectorField());
			world.addChild(vectorFieldBG);
		}
		
		return singularity;
	}
	
	/**
	 * This method toggles the vector field on or off.
	 */
	public void toggleVectorField() {
		if(isVectorField){
			vectorFieldBG.addChild(vectorField());
			world.addChild(vectorFieldBG);
		}
		else {
			world.removeChild(vectorFieldBG);
		}
			
	}

	/**
	 * This method sets the Coefficient of Restitution.
	 * @param CORvalue The new Coefficient of Restitution.
	 */
	public void setCoefficientOfRestitution(float CORvalue)
	{
		cor = CORvalue;
	}
	
	/**
	 * This method resets the forces to the default values.
	 */
	public void resetForces()
	{
		wind = 0;
		gravity = GRAVITY_CONSTANT;
	}
	
	/**
	 * This method is a helper method that deals with the timer
	 * method that allows the program to work in the way it does.
	 */
	@Override
	public void actionPerformed(ActionEvent e) 
	{
		this.stopRenderer();
		tick();
		this.startRenderer();
	}
}
